perf: Optimize CUTLASS MoE helper kernels for small-batch decode workloads - #3014
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughReplaced inline PTX grid-dependency intrinsics with CUDA runtime helpers, added early-exit for experts with zero GEMM work (still triggering programmatic-launch completion), removed N-dimension SF padding loops, adjusted kernel launch sizing and thread counts, and added CUDA-gated tests covering the removed N-dim padding behavior. Changes
Sequence Diagram(s)sequenceDiagram
participant Host as Host
participant Runner as CutlassMoeFCRunner
participant Kernel as CUDA Kernel
participant GridDep as GridDep Runtime
Host->>Runner: prepare params (expanded_num_tokens, num_experts_per_node, k, ...)
Runner->>Kernel: launch kernel (expandInputRows / doActivation / computeStrides)
Kernel->>Kernel: evaluate per-expert tokens_to_expert
alt tokens_to_expert == 0
Kernel->>GridDep: call runtime trigger (cudaTriggerProgrammaticLaunchCompletion)
Kernel-->>Runner: early return (skip per-expert setup)
else tokens present
Kernel->>Kernel: perform per-token K-dim zeroing, stride/pointer setup, compute strides
Kernel-->>Runner: complete writes
end
Runner-->>Host: report completion
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request optimizes Fused MoE kernels by skipping expensive setup for experts with no assigned tokens and refactoring padding operations to be expert-driven and flattened for better thread utilization. Review feedback highlighted a critical unit mismatch in the merged activation loop for FP4/MXFP8, where indexing units for loop bounds and element access are inconsistent, leading to incomplete processing. Additionally, an early exit in the stride computation kernel was flagged for potentially skipping required initialization of problem shapes for experts with zero tokens, which could cause undefined behavior in CUTLASS.
| int64_t const loop_elems = (IsNVFP4 || IsMXFP8) | ||
| ? padded_inter_size / VecSize // cover both real elements and K-dim padding | ||
| : num_elems_in_col; | ||
| bool const do_k_padding = (IsNVFP4 || IsMXFP8) && (padded_inter_size > inter_size); | ||
|
|
||
| ActFn fn{}; | ||
| fn.alpha = gate_alpha; | ||
| fn.beta = gate_beta; | ||
| fn.limit = gate_limit; | ||
| for (int64_t elem_index = start_offset; elem_index < num_elems_in_col; elem_index += stride) { | ||
| for (int64_t elem_index = start_offset; elem_index < loop_elems; elem_index += stride) { | ||
| // K-dim padding region: write zero SF only (no activation compute) | ||
| if (do_k_padding && elem_index >= num_elems_in_col) { | ||
| writeSF<VecSize, VecSize>(num_tokens_before_expert, expert, /*source_row*/ -1, token, | ||
| elem_index, padded_inter_size, fc2_act_sf_flat, | ||
| /* input_sf */ nullptr); | ||
| continue; | ||
| } | ||
|
|
||
| auto fc1_value = | ||
| arrayConvert<GemmResultElem, ComputeElem>(gemm_result_vec[elem_index + gated_off_vec]); | ||
| if (bias_ptr) { |
There was a problem hiding this comment.
There is a critical unit mismatch in the merged activation loop for FP4/MXFP8. loop_elems is calculated in units of VecSize (16 or 32), but the loop body indexes gemm_result_vec and output_vec using elem_index as if it were in units of ACTIVATION_ELEM_PER_THREAD (8).
For example, if inter_size is 1024 and VecSize is 16, loop_elems will be 64. The loop will only iterate 64 times, processing elements 0-511 (since gemm_result_vec[elem_index] accesses 8 elements), leaving the second half of the columns unprocessed. Furthermore, the padding check elem_index >= num_elems_in_col compares values with different units (units of VecSize vs units of 8), leading to incorrect branching between compute and padding logic. This logic needs to be corrected to ensure all elements are processed and indexing is consistent.
There was a problem hiding this comment.
Good catch — you're absolutely right. The main loop indexes in units of ACTIVATION_ELEM_PER_THREAD (8) while the K-dim padding loop indexes in units of VecSize (16 for NVFP4). Merging them into a single loop with padded_inter_size / VecSize as the upper bound causes the main loop to under-iterate (48 iterations instead of 96 for inter_size=768), leaving half the columns unprocessed. The elem_index >= num_elems_in_col check also compares values in different units.
Reverted to the original separate K-dim padding loop in the next commit. In practice, this merge was a no-op for the target workload anyway — Qwen3-30B-A3B has inter_size=768 which is already 128-aligned, so the K-dim padding loop executes zero iterations. The actual performance wins in this PR come from the N-dim padding restructuring and computeStrides changes, which are unaffected.
| if (gemm_m == 0) { | ||
| #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) | ||
| asm volatile("griddepcontrol.launch_dependents;"); | ||
| #endif | ||
| return; | ||
| } |
There was a problem hiding this comment.
The early exit for gemm_m == 0 is placed before the initialization of int4_groupwise_params.shape.problem_shapes (lines 1273-1285). CUTLASS grouped GEMM typically requires all problem shapes to be initialized, even for zero-M problems, to correctly traverse the problem list. Skipping this initialization for experts with no tokens may lead to undefined behavior or illegal memory accesses if the visitor reads uninitialized data from the problem shape buffer. Consider moving the early exit after all problem shape assignments.
There was a problem hiding this comment.
Good point — moved the early exit after all problem shape assignments (both regular and int4_groupwise). CUTLASS's tile scheduler may read problem shapes for all experts to traverse the problem list, so they need to be fully initialized even for zero-M entries. The remaining work after the early exit (alpha scales, block scaling factors, strides, pointers) is only consumed by CUTLASS for experts it actually processes, so skipping those for zero-M experts is safe.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@csrc/fused_moe/cutlass_backend/cutlass_fused_moe_kernels.cuh`:
- Around line 2165-2182: The loop currently computes loop_elems in SF-block
units (loop_elems = padded_inter_size / VecSize) while the body still indexes
activation chunks using num_elems_in_col (which is in ACTIVATION_ELEM_PER_THREAD
units), so most activation chunks are skipped and the do_k_padding branch is
never hit; fix this by making the loop iterate in ACTIVATION_ELEM_PER_THREAD
units (e.g., set loop_elems = padded_inter_size / ACTIVATION_ELEM_PER_THREAD or
restore the separate K-padding loop) and only translate indices to SF-block
units when calling writeSF; update use of loop_elems, num_elems_in_col,
do_k_padding, and the writeSF call so that activation compute and K-padding
checks use the same unit (ACTIVATION_ELEM_PER_THREAD) and convert elem_index to
VecSize/SF-block index only for writeSF/padding writes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: daee07d9-476c-4eb9-b740-4a803176e586
📒 Files selected for processing (1)
csrc/fused_moe/cutlass_backend/cutlass_fused_moe_kernels.cuh
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/moe/test_trtllm_cutlass_fused_moe.py`:
- Around line 2122-2125: The test uses torch.cuda.get_device_capability()
directly to gate NVFP4 skips; replace that direct query with the repo skip
helpers from flashinfer.utils to keep gating consistent: use
get_compute_capability() or the specific predicates like
is_sm100a_supported(...) / is_sm12x_supported(...) (and is_sm90a_supported if
relevant) instead of torch.cuda.get_device_capability(), updating the two skip
decorators (around the blocks shown and the similar one at ~2276-2279) to call
the appropriate helper so the tests follow the established pattern.
- Around line 2188-2213: The test should deterministically poison the temp/SF
scratch path so stale reads fail reliably: before calling
fused_moe.cutlass_fused_moe, fill the output buffer (flash_output) with a
sentinel (e.g., NaN or 0xFF) and, when quantized_input is true, explicitly
allocate and pass an input SF/scratch tensor (the input_sf parameter) filled
with the same sentinel instead of leaving it None; ensure the quant_scales list
and fp4_quantize usage remain unchanged but drive the kernel to use the provided
input_sf so the kernel’s internal SF scratch path is exercised and any
stale-data bug is exposed.
- Line 2148: The inline lambda assigned to round_up causes an E731 violation;
remove that lambda and replace any uses of round_up with the existing ceil_div()
helper already defined in this test file (i.e., delete the line defining
round_up and call ceil_div(x, y) wherever round_up(x, y) was used so behavior
remains identical).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4916041b-893d-4dbc-8d5c-dd270773c319
📒 Files selected for processing (2)
csrc/fused_moe/cutlass_backend/cutlass_fused_moe_kernels.cuhtests/moe/test_trtllm_cutlass_fused_moe.py
| @pytest.mark.skipif( | ||
| torch.cuda.get_device_capability()[0] not in [10, 11, 12], | ||
| reason="NVFP4 is only supported on SM100, SM110 and SM120/SM121", | ||
| ) |
There was a problem hiding this comment.
Use the flashinfer.utils architecture helpers for these skip guards.
These new tests query torch.cuda.get_device_capability() directly even though this file already uses the repo’s skip helpers for the same purpose. Please mirror the nearby is_sm100a_supported(...) / is_sm12x_supported(...) pattern so the gating logic stays consistent across the test suite.
♻️ Suggested change
`@pytest.mark.skipif`(
- torch.cuda.get_device_capability()[0] not in [10, 11, 12],
- reason="NVFP4 is only supported on SM100, SM110 and SM120/SM121",
+ not is_sm100a_supported(torch.device("cuda"))
+ and not is_sm12x_supported(torch.device("cuda")),
+ reason="NVFP4 is only supported on SM100+",
) `@pytest.mark.skipif`(
- torch.cuda.get_device_capability()[0] not in [10, 11, 12],
- reason="MXFP8xMXFP4 is only supported on SM100, SM110 and SM120/SM121",
+ not is_sm100a_supported(torch.device("cuda"))
+ and not is_sm12x_supported(torch.device("cuda")),
+ reason="MXFP8xMXFP4 is only supported on SM100+",
)As per coding guidelines: Use flashinfer.utils functions (get_compute_capability(), is_sm90a_supported(), is_sm100a_supported()) to skip tests on unsupported GPU architectures.
Also applies to: 2276-2279
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/moe/test_trtllm_cutlass_fused_moe.py` around lines 2122 - 2125, The
test uses torch.cuda.get_device_capability() directly to gate NVFP4 skips;
replace that direct query with the repo skip helpers from flashinfer.utils to
keep gating consistent: use get_compute_capability() or the specific predicates
like is_sm100a_supported(...) / is_sm12x_supported(...) (and is_sm90a_supported
if relevant) instead of torch.cuda.get_device_capability(), updating the two
skip decorators (around the blocks shown and the similar one at ~2276-2279) to
call the appropriate helper so the tests follow the established pattern.
| torch.manual_seed(42) | ||
| otype = torch.bfloat16 | ||
| quant_blocksize = 16 | ||
| round_up = lambda x, y: (x + y - 1) // y * y |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists
find . -name "test_trtllm_cutlass_fused_moe.py" -type fRepository: flashinfer-ai/flashinfer
Length of output: 112
🏁 Script executed:
# Read the file to check line 2148 and surrounding context
wc -l tests/moe/test_trtllm_cutlass_fused_moe.pyRepository: flashinfer-ai/flashinfer
Length of output: 115
🏁 Script executed:
# Get the content around line 2148
sed -n '2140,2155p' tests/moe/test_trtllm_cutlass_fused_moe.pyRepository: flashinfer-ai/flashinfer
Length of output: 523
🏁 Script executed:
# Search for ceil_div import or definition in the file
rg "ceil_div" tests/moe/test_trtllm_cutlass_fused_moe.pyRepository: flashinfer-ai/flashinfer
Length of output: 434
🏁 Script executed:
# Check imports at the top of the file
head -50 tests/moe/test_trtllm_cutlass_fused_moe.pyRepository: flashinfer-ai/flashinfer
Length of output: 1610
🏁 Script executed:
# Search for ceil_div definition or import in the file
rg "from.*ceil_div|import.*ceil_div|def ceil_div" tests/moe/test_trtllm_cutlass_fused_moe.pyRepository: flashinfer-ai/flashinfer
Length of output: 104
🏁 Script executed:
# Check where ceil_div is defined - search in the entire codebase
rg "def ceil_div" --type pythonRepository: flashinfer-ai/flashinfer
Length of output: 98
🏁 Script executed:
# Let's check the full import section and look for ceil_div more broadly
rg "ceil_div" -B 5 -A 5 tests/moe/test_trtllm_cutlass_fused_moe.py | head -50Repository: flashinfer-ai/flashinfer
Length of output: 1631
Replace the inline lambda with a reusable ceil_div() call to avoid E731 style violation.
Line 2148 assigns a lambda expression, which triggers Ruff E731. The file already defines ceil_div() for ceiling division, so reuse it:
♻️ Suggested change
- round_up = lambda x, y: (x + y - 1) // y * y
+ def round_up(x: int, y: int) -> int:
+ return ceil_div(x, y) * y📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| round_up = lambda x, y: (x + y - 1) // y * y | |
| def round_up(x: int, y: int) -> int: | |
| return ceil_div(x, y) * y |
🧰 Tools
🪛 Ruff (0.15.9)
[error] 2148-2148: Do not assign a lambda expression, use a def
Rewrite round_up as a def
(E731)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/moe/test_trtllm_cutlass_fused_moe.py` at line 2148, The inline lambda
assigned to round_up causes an E731 violation; remove that lambda and replace
any uses of round_up with the existing ceil_div() helper already defined in this
test file (i.e., delete the line defining round_up and call ceil_div(x, y)
wherever round_up(x, y) was used so behavior remains identical).
| flash_output = torch.zeros_like(x) | ||
|
|
||
| quant_scales = [ | ||
| a1_gs, | ||
| w1_blockscale.view(torch.int32), | ||
| 1.0 / (a1_gs * w1_gs), | ||
| a2_gs, | ||
| w2_blockscale.view(torch.int32), | ||
| 1.0 / (a2_gs * w2_gs), | ||
| ] | ||
| hidden_states = x | ||
| input_sf = None | ||
| if quantized_input: | ||
| hidden_states, input_sf = fp4_quantize(x, a1_gs) | ||
|
|
||
| _ = fused_moe.cutlass_fused_moe( | ||
| hidden_states, | ||
| selected_experts.to(torch.int), | ||
| routing_weights, | ||
| w1_q.contiguous().view(torch.long), | ||
| w2_q.contiguous().view(torch.long), | ||
| otype, | ||
| quant_scales=quant_scales, | ||
| input_sf=input_sf, | ||
| output=flash_output, | ||
| ) |
There was a problem hiding this comment.
Make the stale-SF regression deterministic.
test_moe_nvfp4_ndim_padding_safety never forces the removed SF-padding rows to contain nonzero garbage, and the MXFP8 variant only proves reuse of a separate 64 MiB block before freeing it again. That still does not guarantee the kernel’s internal SF scratch buffers are poisoned, so a stale-read bug can pass depending on allocator state. Please drive the exact temp-buffer path with a known NaN/0xFF fill or a dedicated debug hook instead of relying on opportunistic caching-allocator reuse.
Also applies to: 2327-2337
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/moe/test_trtllm_cutlass_fused_moe.py` around lines 2188 - 2213, The
test should deterministically poison the temp/SF scratch path so stale reads
fail reliably: before calling fused_moe.cutlass_fused_moe, fill the output
buffer (flash_output) with a sentinel (e.g., NaN or 0xFF) and, when
quantized_input is true, explicitly allocate and pass an input SF/scratch tensor
(the input_sf parameter) filled with the same sentinel instead of leaving it
None; ensure the quant_scales list and fp4_quantize usage remain unchanged but
drive the kernel to use the provided input_sf so the kernel’s internal SF
scratch path is exercised and any stale-data bug is exposed.
|
/bot run |
| // For decode (1 token, top_k=8, 128 experts), this skips ~120 of 128 experts. | ||
| if (gemm_m == 0) { | ||
| #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)) | ||
| asm volatile("griddepcontrol.launch_dependents;"); |
There was a problem hiding this comment.
Per suggestion in #2558 (comment), we might use cuda native primitives such as cudaTriggerProgrammaticLaunchCompletion here.
|
/bot run |
|
|
||
| flash_output = torch.zeros_like(x) | ||
|
|
||
| # Poison GPU memory with 0xFF — same approach as the NVFP4 test. |
There was a problem hiding this comment.
I don't see this logic in nvfp4 test?
And can you confirm that if you remove the K dim padding this test catches it?
There was a problem hiding this comment.
I don't see this logic in nvfp4 test?
Sorry that was a stale poisoning attempt. Removed in the latest commit.
I don't see this logic in nvfp4 test?
And can you confirm that if you remove the K dim padding this test catches it?
Yes I can confirm that with the after disapling the K-dim padding and poisoning with 0xFF in commit 6f36003, I see
$ pytest tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size -v --tb=short
============================================================================================ test session starts ============================================================================================
...
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-1] FAILED [ 10%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-4] FAILED [ 20%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-192-1] FAILED [ 30%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-192-4] FAILED [ 40%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-160-192-1] FAILED [ 50%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-160-192-4] FAILED [ 60%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-320-160-1] FAILED [ 70%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-320-160-4] FAILED [ 80%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-256-192-1] PASSED [ 90%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-256-192-4] PASSED [100%]
================================================================================================= FAILURES ==================================================================================================
______________________________________________________________ test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-1] _______________________________________________________________
tests/moe/test_trtllm_cutlass_fused_moe.py:2093: in test_moe_nvfp4_unaligned_hidden_size
torch.testing.assert_close(ref_output, flash_output, rtol=2e-1, atol=2e-1)
E AssertionError: Tensor-likes are not close!
E
E Mismatched elements: 288 / 288 (100.0%)
E Greatest absolute difference: nan at index (0, 0) (up to 0.2 allowed)
E Greatest relative difference: nan at index (0, 0) (up to 0.2 allowed)
______________________________________________________________ test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-4] _______________________________________________________________
tests/moe/test_trtllm_cutlass_fused_moe.py:2093: in test_moe_nvfp4_unaligned_hidden_size
torch.testing.assert_close(ref_output, flash_output, rtol=2e-1, atol=2e-1)
E AssertionError: Tensor-likes are not close!
E
E Mismatched elements: 1152 / 1152 (100.0%)
E Greatest absolute difference: nan at index (0, 0) (up to 0.2 allowed)
E Greatest relative difference: nan at index (0, 0) (up to 0.2 allowed)
...
...
========================================================================================== short test summary info ==========================================================================================
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-1] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-4] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-192-1] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-192-4] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-160-192-1] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-160-192-4] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-320-160-1] - AssertionError: Tensor-likes are not close!
FAILED tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-320-160-4] - AssertionError: Tensor-likes are not close!
================================================================================== 8 failed, 2 passed in 361.01s (0:06:01) ==================================================================================
i.e., the carefully chosen hidden sizes cause failures with NaNs produced.
After reverting the changes in ab0068c, I get back
$ pytest tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size -v --tb=short
============================================================================================ test session starts ============================================================================================
...
...
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-1] PASSED [ 10%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-128-4] PASSED [ 20%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-192-1] PASSED [ 30%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-288-192-4] PASSED [ 40%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-160-192-1] PASSED [ 50%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-160-192-4] PASSED [ 60%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-320-160-1] PASSED [ 70%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-320-160-4] PASSED [ 80%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-256-192-1] PASSED [ 90%]
tests/moe/test_trtllm_cutlass_fused_moe.py::test_moe_nvfp4_unaligned_hidden_size[swiglu-False-otype0-wtype0-2-2-256-192-4] PASSED [100%]
which is the current state
|
/bot run |
Ports optimizations from flashinfer-ai/flashinfer#3014 for small-batch decode. For NVFP4/MXFP8 MoE decode with 128 experts and top_k=8, ~120 experts are empty, and the helper kernels were walking all of them or zero-padding scale factors that CUTLASS grouped GEMM never reads. - computeStridesTmaWarpSpecializedKernel: early-exit threads whose assigned expert has gemm_m == 0 after problem-shape init, skipping alpha-scale / block-scaling-factor / stride / pointer setup. - Reduce stride-kernel threadblock from min(1024, experts) to min(32, experts) so 128 experts span 4 SMs instead of 1. - Remove N-dim SF padding loops from expandInputRowsKernel and doActivationKernel and drop the corresponding num_padding_tokens term from both launchers. CUTLASS sets gemm_m = tokens_to_expert per expert and never reads SFs for rows beyond that; K-dim SF padding is still required and remains. Reported ~5-12% decode speedup on RTX Pro 6000 and 2-6% on Spark, neutral elsewhere. Exercised by existing 128-expert NVFP4 / W4A8_NVFP4_FP8 / W4A8_MXFP4_MXFP8 coverage in tests/unittest/_torch/modules/moe/test_moe_backend.py and cpp/tests/unit_tests/kernels/mixtureOfExpertsTest.cu. Signed-off-by: Pamela <179191831+pamelap-nvidia@users.noreply.github.com> [None][test] Cover inactive-expert paths in CUTLASS MoE gtest Adds four TYPED_TESTs to mixtureOfExpertsTest.cu exercising the code paths touched by the prior commit. All four leave mIsLongTest at its default (false), so BasicPermuteTest / ParallelismTest run the memset-poison determinism check (0xD5 vs 0x2A) that validates output independence from uninitialised workspace. - PermuteManyInactiveExperts: k=8, 128 experts, 3 tokens. Most experts receive no tokens, exercising the gemm_m == 0 early-exit and, for block-scaled dtypes (NVFP4, MXFP8xMXFP4), the removed N-dim SF padding region. - PermuteSingleTokenDecode: k=8, 128 experts, 1 token. Every active expert gets gemm_m == 1 (tightest valid case) alongside ~120 inactive experts. - PermuteSwigluManyInactiveExperts: same many-inactive profile with Swiglu activation, exercising the block-scaled GLU path in doActivationKernel on MXFP8xMXFP4. - ExpertParallelManyInactiveExperts: shards 128 experts across 64 or 128 ranks with k=8 and 3 tokens; many ranks see zero active tokens, end-to-end including the alltoall-adjacent flow. Signed-off-by: Pamela <179191831+pamelap-nvidia@users.noreply.github.com> [None][fix] Guard inactive-expert early-exit and Swiglu test Two follow-up fixes for the H100 regression surfaced in CUTLASS MoE gtest: - Gate the gemm_m == 0 early exit in computeStridesTmaWarpSpecializedKernel on !int4_groupwise_params.enabled. The W4A8_AWQ / WFP4A16 CUTLASS kernel instantiations read per-expert stride_s_a / ptr_s_a during init regardless of per-group tile count, so skipping setup left those arrays uninitialised and crashed PermuteSweepNumTokensGeglu / misresulted PermuteSweepNumTokens and PermuteMixtral8x7b on FP8+uint4+bf16. The optimisation still applies to NVFP4, MXFP8xMXFP4, FP8, BF16, and FP16 paths where FlashInfer validated the safety of the early exit. - Skip PermuteSwigluManyInactiveExperts for half output. FP16's narrow dynamic range can't hold top_k=8 Swiglu accumulation at the minimum- alignment hidden size within compareFinal's tolerance. Mirrors the existing NVFP4 Relu-only skip in BasicPermuteTest. BF16 and MXFP8xMXFP4 instances continue to run and retain coverage. Signed-off-by: Pamela <179191831+pamelap-nvidia@users.noreply.github.com> [None][chore] Bump copyright year on modified CUTLASS MoE files Signed-off-by: Pamela <179191831+pamelap-nvidia@users.noreply.github.com>
Ports optimizations from flashinfer-ai/flashinfer#3014 for small-batch decode. For NVFP4/MXFP8 MoE decode with 128 experts and top_k=8, ~120 experts are empty, and the helper kernels were walking all of them or zero-padding scale factors that CUTLASS grouped GEMM never reads. - computeStridesTmaWarpSpecializedKernel: early-exit threads whose assigned expert has gemm_m == 0 after problem-shape init, skipping alpha-scale / block-scaling-factor / stride / pointer setup. - Reduce stride-kernel threadblock from min(1024, experts) to min(32, experts) so 128 experts span 4 SMs instead of 1. - Remove N-dim SF padding loops from expandInputRowsKernel and doActivationKernel and drop the corresponding num_padding_tokens term from both launchers. CUTLASS sets gemm_m = tokens_to_expert per expert and never reads SFs for rows beyond that; K-dim SF padding is still required and remains. Reported ~5-12% decode speedup on RTX Pro 6000 and 2-6% on Spark, neutral elsewhere. Exercised by existing 128-expert NVFP4 / W4A8_NVFP4_FP8 / W4A8_MXFP4_MXFP8 coverage in tests/unittest/_torch/modules/moe/test_moe_backend.py and cpp/tests/unit_tests/kernels/mixtureOfExpertsTest.cu. Signed-off-by: Pamela <179191831+pamelap-nvidia@users.noreply.github.com>
📌 Description
Summary
Ported optimizations from TRTLLM and expanded a bit:
expandInputRowsKernelanddoActivationKernel— the CUTLASS grouped GEMM never reads scale factors beyondtokens_to_expertcomputeStridesTmaWarpSpecializedKernelto skip stride/pointer setup for experts with no assigned tokenscomputeStrideswork across multiple SMs (4 blocks instead of 1)__launch_bounds__todoActivationKernelfor better register allocationMotivation
When running CUTLASS MoE during the generation phase, three MoE helper kernels were severely underutilized:
expandInputRowsKerneldoActivationKernelcomputeStridesTmaWarpSpecializedKernelThe N-dim SF padding loops iterated over
MinNDimAlignment x num_experts = 128 x 128 = 16384potential padding slots to zero scale factors for token rows beyond each expert's actual token count. This padding was unnecessary — the CUTLASS grouped GEMM setsgemm_m = tokens_to_expertper expert and never reads scale factors for rows beyond that boundary.Changes
expandInputRowsKernel+doActivationKernel-- Remove N-dim SF padding:Deleted the entire N-dim SF padding section (after
griddepcontrol.launch_dependents) from both kernels. The CUTLASS grouped GEMM's problem shapes bound the MMA tile access totokens_to_expertrows per expert; padding rows are never read. Removed deadnum_padding_tokensvariables from both launchers and simplified grid formulas to be driven purely by the expanded token count.K-dim SF padding (inside the per-token main loop) is preserved — MMA tiles can straddle the
inter_sizeboundary within valid rows, requiring those positions to be zeroed.doActivationKernel--__launch_bounds__:Added
__launch_bounds__(ACTIVATION_THREADS_PER_BLOCK)to help the compiler optimize register allocation.computeStridesTmaWarpSpecializedKernel-- M=0 early exit:After writing
problem_shapes[expert]andint4_groupwise_params.shape(which CUTLASS needs for all experts to traverse the problem list), experts withgemm_m == 0return immediately — skippingsetupFP4BlockScalingFactors,computeTmaWarpSpecializedInputStrides, andcomputeTmaWarpSpecializedInputPointersfor both GEMMs. For decode with 128 experts and top_k=8, this skips ~120 experts' full setup.computeStridesTmaWarpSpecializedKernel-- block size:Changed
std::min(1024, num_experts_per_node)tostd::min(32, num_experts_per_node), spreading 128 experts across 4 blocks on 4 SMs instead of 1 block on 1 SM.Correctness
The N-dim padding removal was validated with 0xFF poisoning: the SF buffer was filled with 0xFF (worst-case FP8 scale factor values of +/-448) before the kernel wrote real SFs to valid positions. With padding positions containing 0xFF, all tests pass with the same error rates as the original code — confirming the CUTLASS GEMM never reads the padding positions.
K-dim SF padding is preserved because MMA tiles straddle the K boundary within valid rows. The original author confirmed: "Tests should fail if the K dimension padding is disabled, but not if the N dimension stuff is."
All changes preserve PDL (
griddepcontrol) overlap — nocudaMemsetAsyncor stream operations are introduced.Large-batch regression safety
All optimizations are unconditionally beneficial or neutral for large batches:
expanded_tokensdominates and the grid remains atsmCount * 8__launch_bounds__: unconditionally betterPerformance Numbers
Click to view `flashinfer_benchmark.py` test cases used to collect the data
Click to view perf data on RTX Pro 6000 SM120) and Spark (SM121)
Perf data demonstrated that untargeted cases have no performance impact. Targeted (NVFP4) CUTLASS MoE cases see speedup on decode cases.
Summary table:
Full table:
🔍 Related Issues
#3013
🚀 Pull Request Checklist
Thank you for contributing to FlashInfer! Before we review your pull request, please make sure the following items are complete.
✅ Pre-commit Checks
pre-commitby runningpip install pre-commit(or used your preferred method).pre-commit install.pre-commit run --all-filesand fixed any reported issues.🧪 Tests
unittest, etc.).Reviewer Notes
Summary by CodeRabbit